<template>
  <div id="game">
    <div class="game_header">
      <h3 class="title">数独游戏</h3>
      <p class="game_setting">
        游戏难度：{{ levelText[levelIndex] || '未选择' }}
        <span></span>剩余游戏时间：{{ time }}s
      </p>
    </div>

    <div class="sudoku_wrapper">
      <div class="sudoku_area" :class="{ 'sudoku_start': gamestart }">
        <div class="sudoku_row" v-for="(row, index) in rowList" :key="index">
          <div class="sudoku_cell" v-for="(value, cellIndex) in row" :key="cellIndex">
            <input type="text" 
              :value="abandon || typeof cellValueList['cell-' + index + '-' + cellIndex] == 'undefined' ? value : cellValueList['cell-' + index + '-' + cellIndex]" 
              :readonly="abandon || typeof cellValueList['cell-' + index + '-' + cellIndex] == 'undefined' "
              @input="checkInput($event, index, cellIndex)">
          </div>
        </div>
      </div>
    </div>

    <div class="game_button">
      <button id="start_btn" v-if="!gamestart" @click="popupShow=true">开始游戏</button>
      <button id="fin_btn" v-if="gamestart&&!gameover" @click="judgeSudoku">提交答案</button>
      <button id="quit_btn" v-if="gamestart&&!gameover" @click="abandonAnswer">放弃答题</button>
      <button id="restart_btn" v-if="gamestart&&gameover" @click="resetGame">重新开始</button>
      <button id="level_btn" v-if="gamestart&&gameover" @click="popupShow=true">选择难度</button>
    </div>

    <div class="popup_mask" v-show="popupShow">
      <div class="popup_wrap">
        <h2 class="title">选择难度</h2>
        <div class="level_wrap">
          <div class="level_item" :class="{ 'selected': index === levelIndex }"
            v-for="(item, index) in levelText" :key="index"
            @click="chooseLevel(index)">
            {{ item }}
          </div>
        </div>
        <div class="popup_msg">当前难度单句游戏限时{{ time }}秒，单个数独块隐藏概率{{ levelIndex * 10 + 20 }}%</div>
        <div class="popup_button">
          <button @click="start">开始</button>
          <span></span>
          <button @click="popupShow=false">取消</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'game',
  data() {
    return {
      rowList: [], // 生成的数独
      gamestart: false, // 游戏开始
      gameover: true, // 游戏结束
      abandon: false, // 放弃游戏
      levelText: [ '简单', '普通', '困难', '极难' ], // 游戏难度
      levelIndex: -1, // 游戏难度等级
      time: '--', // 游戏限时
      popupShow: false, // 难度选择弹窗是否显示,
      cellValueList: {}, // 需要填入数字的格子集合
    }
  },
  methods: {
    start() {
      if(this.levelIndex > -1) {
        this.popupShow = false
        if(!this.gamestart) {
          this.gamestart = true
          this.gameover = false
          this.rowList.length = 9
          for(let i=0; i<9; i++) {
            const arr = new Array(9)
            this.rowList[i] = arr
          }
          setTimeout(() => {
            this.createSudoku()
          }, 1500)
        } else {
          this.resetGame()
        }
      } else {
        alert('请选择一个难度')
      }
    },
    resetGame() {
      this.rowList = []
      this.cellValueList = {}
      this.time = 720 - this.levelIndex * 120
      this.gameover = false
      this.abandon = false
      this.createSudoku()
    },
    // 初始化数独列表
    // 生成的不符合要求会重新生成
    createSudoku() {
      this.getKeyBlock()
      this.getOtherBlock([2], [6], 0)
      if(this.rowList[0].filter(val => val).length < 9) {
        this.createSudoku()
        return
      }
      this.getOtherBlock([0, 2], [4], 1)
      if(this.rowList[1].filter(val => val).length < 9) {
        this.createSudoku()
        return
      }
      this.getOtherBlock([4], [0, 6], 3)
      if(this.rowList[3].filter(val => val).length < 9) {
        this.createSudoku()
        return
      }
      this.getOtherBlock([3, 4], [2], 5)
      if(this.rowList[5].filter(val => val).length < 9) {
        this.createSudoku()
        return
      }
      this.getOtherBlock([6], [1, 4], 7)
      if(this.rowList[7].filter(val => val).length < 9) {
        this.createSudoku()
        return
      }
      this.getOtherBlock([6, 7], [2, 5], 8)
      if(this.rowList[8].filter(val => val).length < 9) {
        this.createSudoku()
        return
      }
      // 随机生成可以填写的数独块
      this.courseRating()
      this.countDown()
    },
    // 随机生成可以填写的数独块
    courseRating() {
      for(let index=0; index<9; index++) {
        for(let cellIndex=0; cellIndex<9; cellIndex++) {
          const edit = Math.floor(Math.random() * 100) < this.levelIndex * 10 + 20
          if(edit) {
            this.cellValueList['cell-' + index + '-' + cellIndex] = ''
          }
        }
      }
    },
    // 检查输入，只能输入1-9的数字
    checkInput(e, index, cellIndex) {
      const val = e.target.value.substring(0, 1).replace(/0|\D/g, '')
      this.cellValueList['cell-' + index + '-' + cellIndex] = val
      e.target.value = val
    },
    // 倒计时
    countDown() {
      const _this = this
      if(this.time <= 0) {
        alert('时间到，游戏结束')
        this.gameover = true
      } else if(!this.abandon) {
        setTimeout(() => {
          _this.time--
          _this.countDown()
        },1000)
      }
    },
    // 切换难度
    chooseLevel(index) {
      this.levelIndex = index
      this.time = 720 - this.levelIndex * 120
    },
    // 初始化九宫格[2, 4, 6]位置的数据
    getKeyBlock() {
      const keyArr = [ 2, 4, 6 ]
      keyArr.forEach(item => {
        const row = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
        this.rowList[item] = row.sort(() => Math.random() < 0.5 ? -1 : 1)
      })
    },

    // 以关键位置[2, 4, 6]初始化其他位置数据
    // 传入 同行已生成的九宫格数组下标，同列已生成数组下标，当前九宫格下标
    getOtherBlock(rowIndexArr, colIndexArr, index) {
      // 把同行九宫格数据拆成二维数组用来判断重复
      const rowList = this.sameRowMatrix(rowIndexArr)
      // 把同列九宫格数据拆成二维数组用来判断重复
      const colList = this.sameColMatrix(colIndexArr)
      // 把每个格子的可选数字放入到一个二维数组中
      const resultArr = this.getFinMatrix(rowList, colList)
      // 初始化当前下标九宫格数据
      this.getResultArr(resultArr, index)
    },

    // 封装 把同行九宫格数据拆成二维数组用来判断重复
    sameRowMatrix(rowIndexArr) {
      const rowList = []
      rowIndexArr.forEach((item, i) => {
        const rowArr = this.rowList[item]
        rowList[i] = [ [rowArr[0], rowArr[1], rowArr[2]], 
                      [rowArr[3], rowArr[4], rowArr[5]], 
                      [rowArr[6], rowArr[7], rowArr[8]] ]
      })
      return rowList
    },
    // 封装 把同列九宫格数据拆成二维数组用来判断重复
    sameColMatrix(colIndexArr) {
      const colList = []
      colIndexArr.forEach((item, i) => {
        const colArr = this.rowList[item]
        colList[i] = [ [colArr[0], colArr[3], colArr[6]],
                      [colArr[1], colArr[4], colArr[7]],
                      [colArr[2], colArr[5], colArr[8]] ]
      })
      return colList
    },
    getFinMatrix(rowList, colList) {
      const resultArr = []
      const row = [1, 2, 3, 4, 5, 6, 7, 8, 9]
      // 筛选出当前下标的九宫格每个格子的可填入数字
      for(let i=0; i<9; i++) {
        const rowIndex = parseInt(i / 3) // 用来比较的行下标
        const colIndex = i % 3 // 用来比较的列下标
        // 去重
        const getUsableValue = function(value) {
          let count = 0 // 重复次数
          // 当 当前格子的数据 与 已生成的同行或同列九宫格的 同行或同列数据相同时，重复次数加1
          rowList.forEach((item, i) => {
            if(rowList[i][rowIndex].some(val => val===value))
              count++
          })
          colList.forEach((item, i) => {
            if(colList[i][colIndex].some(val => val===value))
              count++
          })
          return count < 1
        }
        resultArr[i] = row.filter(getUsableValue)
      }
      return resultArr
    },
    // 根据单个九宫格块每个格子的可能值选出合适的值填入
    getResultArr(resultArr, index) {
      const realArr = new Array(9)
      const numArr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
      for(let i=0; i<9; i++){
        const num = this.judgeCount(numArr, resultArr)
        let index = -1
        let minlength = 9
        resultArr.forEach((item,j) => {
          // 判断可填数字数组中是否包含当前填入数字
          // 如果包含，则在当前格子可填数字中移除已填入数字
          if(item.indexOf(num) > -1) {
            // 判断每个格子可填数字个数，优先给最少的填入
            if(minlength >= item.length) {
              minlength = item.length
              index = j
            }
            item.splice(item.indexOf(num), 1)
          }
        })
        // 移除已用掉的数字
        numArr.splice(numArr.indexOf(num), 1)
        // 判断当前九宫格的数据是否都符合规则
        // 如果不符合规则会出现empty
        // 符合规则就将数据导入当前九宫格数组realArr
        if(index > -1) {
          resultArr[index] = []
          realArr[index] = num
        }
      }
      this.rowList.splice(index, 1, realArr)
    },
    // 用来把可填数字中最少的数字优先填入
    judgeCount(numArr, resultArr) {
      const onlyNum = {
        num: 0,
        count: 9
      }
      numArr.forEach(index => {
        let count = 0
        resultArr.forEach(item => {
          if(item.indexOf(index) > -1) {
            count ++
          }
        })
        if(count < onlyNum.count) {
          onlyNum.num = index,
          onlyNum.count = count
        }
      })
      return onlyNum.num
    },
    // 判断数独填写结果
    judgeSudoku() {
      const answer = this.rowList
      // 遍历所有需要填写格子的值，如果值存在，则将其对应位置的值加入验证结果
      for(const key in this.cellValueList) {
        const element = this.cellValueList[key]
        if(element) {
          const keyIndex = key.split('-')
          answer[keyIndex[1]][keyIndex[2]] = parseInt(element)
        } else {
          alert('数独未填完')
          return
        }
      }
      if(this.judgeRow(answer) && this.judgeCol(answer) && this.judgeBlock(answer)){
        alert('恭喜答对')
        this.gameover = true
      } else {
        alert('答案有误，再仔细看看')
      }
    },
    // 判断每一行是否符合数独规则
    judgeRow(answer) {
      for(let i=0; i<9; i+=3) {
        for(let j=0; j<9; j+=3) {
          const arr = (answer[i].slice(j, j+3) + ',' 
                      + answer[i+1].slice(j, j+3) + ',' 
                      + answer[i+2].slice(j, j+3)).split(',')
          if(this.isRepeat(arr)) {
            return false
          }
        }
      }
      return true
    },
    // 判断每一列是否符合数独规则
    judgeCol(answer) {
      for(let i=0; i<3 ;i++) {
        for(let j=0; j<3; j++) {
          const arr = [ answer[i][j], answer[i][j+3], answer[i][j+6],
                        answer[i+3][j], answer[i+3][j+3], answer[i+3][j+6],
                        answer[i+6][j], answer[i+6][j+3], answer[i+6][j+6] ]
          if(this.isRepeat(arr)) {
            return false
          }
        }
      }
      return true
    },
    // 判断每一个九宫格是否符合数独规则
    judgeBlock(answer) {
      for(let i=0; i<9; i++) {
        if(this.isRepeat(answer[i])) {
          return false
        }
      }
      return true
    },
    // 判断数组中数据是否重复
    isRepeat(arr) {
      return new Set(arr).size < 9
    },
    // 放弃答题
    abandonAnswer() {
      const abandon = confirm('确定放弃此次答题？')
      if(abandon) {
        this.abandon = abandon
        this.gameover = true
      }
    }
  }
}
</script>

<style lang="less" scoped>

  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  #game {
    min-height: calc(100vh - 90px);
    background: #fff;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    position: relative;
    .game_header {
      text-align: center;
      padding-top: 10px;
      margin-bottom: 20px;
      .title {
        margin-bottom: 10px;
      }
      .game_setting {
        font-size: 16px;
        span {
          width: 2rem;
          display: inline-block;
        }
      }
    }
    .sudoku_wrapper {
      padding: 0 10px;
      height: 27rem;
      width: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      .sudoku_area {
        width: 100%;
        height: 0;
        box-shadow: 0 0 1px 2px #000;
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        overflow-y: hidden;
        transition: all 2s;
        .sudoku_row {
          width: 33.33333333%;
          display: flex;
          flex-wrap: wrap;
          .sudoku_cell {
            width: 33.33333333%;
            height: 3rem;
            line-height: 3rem;
            text-align: center;
            border: 1px solid #aaa;
            input {
              width: 100%;
              height: 100%;
              padding: 0;
              margin: 0;
              color: #300daf;
              border: 0;
              background: transparent;
              text-align: center;
              font-size: 20px;
              font-weight: 600;
            }
            input:read-only {
              color: #000;
              font-size: 16px;
              font-weight: 500;
            }
          }
        }
        .sudoku_row:nth-child(2) {
          .sudoku_cell:nth-child(1) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(4) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(7) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(3) {
            border-right-color: #000;
          }
          .sudoku_cell:nth-child(6) {
            border-right-color: #000;
          }
          .sudoku_cell:nth-child(7) {
            border-bottom-color: #000;
          }
          .sudoku_cell:nth-child(8) {
            border-bottom-color: #000;
          }
          .sudoku_cell:nth-child(9) {
            border-bottom-color: #000;
            border-right-color: #000;
          }
        }
        .sudoku_row:nth-child(8) {
          .sudoku_cell:nth-child(1) {
            border-left-color: #000;
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(4) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(2) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(7) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(3) {
            border-right-color: #000;
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(6) {
            border-right-color: #000;
          }
          .sudoku_cell:nth-child(9) {
            border-right-color: #000;
          }
        }
        .sudoku_row:nth-child(5) {
          .sudoku_cell:nth-child(1) {
            border-left-color: #000;
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(2) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(4) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(3) {
            border-right-color: #000;
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(6) {
            border-right-color: #000;
          }
          .sudoku_cell:nth-child(7) {
            border-bottom-color: #000;
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(8) {
            border-bottom-color: #000;
          }
          .sudoku_cell:nth-child(9) {
            border-bottom-color: #000;
            border-right-color: #000;
          }
        }
        .sudoku_row:nth-child(6) {
          .sudoku_cell:nth-child(1) {
            border-left-color: #000;
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(2) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(3) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(4) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(7) {
            border-bottom-color: #000;
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(8) {
            border-bottom-color: #000;
          }
          .sudoku_cell:nth-child(9) {
            border-bottom-color: #000;
          }
        }
        .sudoku_row:nth-child(3) {
          .sudoku_cell:nth-child(1) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(4) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(7) {
            border-bottom-color: #000;
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(8) {
            border-bottom-color: #000;
          }
          .sudoku_cell:nth-child(9) {
            border-bottom-color: #000;
          }
        }
        .sudoku_row:nth-child(1) {
          .sudoku_cell:nth-child(3) {
            border-right-color: #000;
          }
          .sudoku_cell:nth-child(6) {
            border-right-color: #000;
          }
          .sudoku_cell:nth-child(7) {
            border-bottom-color: #000;
          }
          .sudoku_cell:nth-child(8) {
            border-bottom-color: #000;
          }
          .sudoku_cell:nth-child(9) {
            border-bottom-color: #000;
            border-right-color: #000;
          }
        }
        .sudoku_row:nth-child(4) {
          .sudoku_cell:nth-child(1) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(2) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(3) {
            border-right-color: #000;
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(6) {
            border-right-color: #000;
          }
          .sudoku_cell:nth-child(7) {
            border-bottom-color: #000;
          }
          .sudoku_cell:nth-child(8) {
            border-bottom-color: #000;
          }
          .sudoku_cell:nth-child(9) {
            border-bottom-color: #000;
            border-right-color: #000;
          }
        }
        .sudoku_row:nth-child(7) {
          .sudoku_cell:nth-child(1) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(2) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(3) {
            border-top-color: #000;
            border-right-color: #000;
          }
          .sudoku_cell:nth-child(6) {
            border-right-color: #000;
          }
          .sudoku_cell:nth-child(9) {
            border-right-color: #000;
          }
        }
        .sudoku_row:nth-child(9) {
          .sudoku_cell:nth-child(1) {
            border-top-color: #000;
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(2) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(3) {
            border-top-color: #000;
          }
          .sudoku_cell:nth-child(4) {
            border-left-color: #000;
          }
          .sudoku_cell:nth-child(7) {
            border-left-color: #000;
          }
        }
      }
      .sudoku_start {
        height: 27rem;
      }
    }
    .game_button {
      margin-top: 15px;
      button {
        padding: 0.5rem 1rem;
        border-radius: 4px;
        border: 1px solid #000;
        color: #fff;
        text-align: center;
        margin: 0 2rem;
      }
      #start_btn {
        background: #00cd66;
      }
      #fin_btn {
        background: #3333ff;
      }
      #quit_btn {
        background: #ff0000;
      }
      #restart_btn {
        background: #00cd66;
      }
      #level_btn {
        background: #eeee00;
      }
    }
    .popup_mask {
      width: 100%;
      max-width: 68vh;
      height: 100%;
      background: rgba(0, 0, 0, .5);
      overflow: hidden;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      .popup_wrap {
        background: #fff;
        width: 80%;
        border-radius: 4px;
        text-align: center;
        padding: 20px;
        .level_wrap {
          display: flex;
          justify-content: space-between;
          margin: 10px 20px;
          .level_item {
            padding: 5px 10px;
            border-radius: 4px;
            cursor: pointer;
          }
          .level_item:hover {
            background: #ccc;
          }
          .level_item.selected {
            background: #3333ff;
            color: #fff;
          }
          .level_item.selected:hover {
            background: #3333ff;
          }
        }
        .popup_msg {
          font-size: 12px;
          color: #bbb;
          margin-bottom: 15px;
        }
        .popup_button {
          display: flex;
          justify-content: center;
          button {
            border-radius: 4px;
            padding: 5px 10px;
            margin-right: 10px;
            cursor: pointer;
          }
          button:first-child {
            background: #3333ff;
            color: #fff;
          }
          span {
            display: inline-block;
            width: 1rem;
          }
        }
      }
    }
  }
</style>
